/**
 * lcd1602.h - LCD1602 驱动库 - 头文件
 * by Nixsawe <ziming_cool@126.com>
 * helped by @slipperstree
 * 
 * 为了简化操作逻辑，特封装此库
 */

#ifndef __LCD1602__
#define __LCD1602__

#include<atmel/at89x52.h>
#include<intrins.h>


/*----------  全局配置  ----------*/



/*----------  数据总线长度  ----------*/

// 数据总线长度只能为 4 或 8，且在编译时确定，运行时无法修改
// 默认的数据总线长度为 8

#ifdef LCD1602_DATA_LENGTH
# if (LCD1602_DATA_LENGTH==8)
# elif (LCD1602_DATA_LENGTH==4)
# else
// 无效的总线长度，总线长度只能为 4 或 8
#  undef LCD1602_DATA_LENGTH
#  pragma warning("Invalid macro LCD1602_DATA_LENGTH. Only 4 or 8 is acceptable.")
# endif
#endif

#ifndef LCD1602_DATA_LENGTH
# define LCD1602_DATA_LENGTH 8
#endif

#undef LCD1602_DATALINE_4_BITS
#undef LCD1602_DATALINE_8_BITS

#if (LCD1602_DATA_LENGTH==8)
// 8 位数据总线
# define LCD1602_DATALINE_8_BITS
# undef LCD1602_DATALINE_USE_PORT
#else
// 4 位数据总线，写 1 个字节要在 D7~D4 上输出两次 4bit 数据
# define LCD1602_DATALINE_4_BITS
#endif


/*----------  输入输出引脚  ----------*/

// LCD 使能位，这条线上产生上升沿将导致命令被执行
// 也称为时钟线 (响应时间 <=25ns，脉冲宽度 >=150ns，信号周期 >=400ns)
// 几乎不需要在时序上考虑等待的问题
#ifndef LCD1602_EN
# define LCD1602_EN P2_7
#endif

// LCD 数据/命令选择位 (H/L)
// 高电平时读写数据，低电平时读状态或写命令
#ifndef LCD1602_RS
# define LCD1602_RS P2_6
#endif

// LCD 读/写选择位 (H/L)
// 高电平读数据，低电平写数据
#ifndef LCD1602_RW
# define LCD1602_RW P2_5
#endif

// LCD Data IO 端口
// 数据输入输出
#ifndef LCD1602_IO_PORT
# define LCD1602_IO_PORT P0
#endif

// LCD Data IO 端口 (高 4 位)
#ifndef LCD1602_IO_D7
# define LCD1602_IO_D7 P0_7
#endif
#ifndef LCD1602_IO_D6
# define LCD1602_IO_D6 P0_6
#endif
#ifndef LCD1602_IO_D5
# define LCD1602_IO_D5 P0_5
#endif
#ifndef LCD1602_IO_D4
# define LCD1602_IO_D4 P0_4
#endif


/*----------  基本类型定义  ----------*/

typedef unsigned char LCD1602_CMD;
typedef unsigned char LCD1602_DATA;
typedef unsigned char LCD1602_STATE;

// LCD1602 初始化结构体类型
typedef struct {
	// 显示行数、字体大小
	LCD1602_CMD FNS;
	// 屏幕显示与光标
	LCD1602_CMD DSP;
	// 输入模式
	LCD1602_CMD ENT;
} LCD1602_INIT_DATA;


/*----------  基础功能封装  ----------*/

// 等待一个机器周期 (约 1μs)
#define lcd1602_wait_nop() \
	do \
	{ \
		_nop_(); \
	} while(0)

// 使能线上升沿
#define lcd1602_rising_edge() \
	do \
	{ \
		LCD1602_EN = 1; \
		lcd1602_wait_nop(); \
	} while(0)

// 使能线下降沿
#define lcd1602_falling_edge() \
	do \
	{ \
		LCD1602_EN = 0; \
		lcd1602_wait_nop(); \
	} while(0)


#if defined(LCD1602_DATALINE_8_BITS)

// 8 位总线写
#define lcd1602_write(dat) \
	do \
	{ \
		LCD1602_IO_PORT = (dat); \
		lcd1602_wait_nop(); \
		lcd1602_rising_edge(); \
		lcd1602_falling_edge(); \
	} while(0)
// 8 位总线读
#define lcd1602_read(dat) \
	do \
	{ \
		LCD1602_IO_PORT = 0xff; \
		lcd1602_wait_nop(); \
		lcd1602_rising_edge(); \
		(dat) = LCD1602_IO_PORT; \
		lcd1602_falling_edge(); \
	} while(0)

#elif defined(LCD1602_DATALINE_USE_PORT)
// 4 位总线，使用整个输出端口的高 4 位进行输出
// 如果低 4 位作为其他功能使用，可能造成干扰

// 4 位总线写
#define lcd1602_write(dat) \
	do \
	{ \
		/* 写高 4 位 */ \
		LCD1602_IO_PORT = ((dat) & 0xf0); \
		lcd1602_wait_nop(); \
		lcd1602_rising_edge(); \
		lcd1602_falling_edge(); \
		/* 写低 4 位 */ \
		LCD1602_IO_PORT = ((dat) << 4); \
		lcd1602_wait_nop(); \
		lcd1602_rising_edge(); \
		lcd1602_falling_edge(); \
	} while(0)
// 4 位总线读
#define lcd1602_read(dat) \
	do \
	{ \
		/* 读高 4 位 */ \
		LCD1602_IO_PORT = 0xf0; \
		lcd1602_wait_nop(); \
		lcd1602_rising_edge(); \
		(dat) = (LCD1602_IO_PORT & 0xf0); \
		lcd1602_falling_edge(); \
		/* 读低 4 位 */ \
		LCD1602_IO_PORT = 0xf0; \
		lcd1602_wait_nop(); \
		lcd1602_rising_edge(); \
		(dat) |= (LCD1602_IO_PORT >> 4); \
		lcd1602_falling_edge(); \
	} while(0)

#else
// 4 位总线，输出时按位寻址独立输出 4 位数据
// 不会造成干扰

// 4 位总线写
#define lcd1602_write(dat) \
	do \
	{ \
		/* 写高 4 位 */ \
		LCD1602_IO_D7 = ((dat) >> 7) & 1; \
		LCD1602_IO_D6 = ((dat) >> 6) & 1; \
		LCD1602_IO_D5 = ((dat) >> 5) & 1; \
		LCD1602_IO_D4 = ((dat) >> 4) & 1; \
		lcd1602_wait_nop(); \
		lcd1602_rising_edge(); \
		lcd1602_wait_nop(); \
		lcd1602_falling_edge(); \
		/* 写低 4 位 */ \
		LCD1602_IO_D7 = ((dat) >> 3) & 1; \
		LCD1602_IO_D6 = ((dat) >> 2) & 1; \
		LCD1602_IO_D5 = ((dat) >> 1) & 1; \
		LCD1602_IO_D4 = (dat) & 1; \
		lcd1602_wait_nop(); \
		lcd1602_rising_edge(); \
		lcd1602_wait_nop(); \
		lcd1602_falling_edge(); \
	} while(0)
// 4 位总线读
#define lcd1602_read(dat) \
	do \
	{ \
		/* 读高 4 位 */ \
		LCD1602_IO_D7 = 1; \
		LCD1602_IO_D6 = 1; \
		LCD1602_IO_D5 = 1; \
		LCD1602_IO_D4 = 1; \
		lcd1602_wait_nop(); \
		lcd1602_rising_edge(); \
		lcd1602_wait_nop(); \
		(dat) = (((unsigned char)(LCD1602_IO_D7) << 7) | ((unsigned char)(LCD1602_IO_D6) << 6) | ((unsigned char)(LCD1602_IO_D5) << 5) | ((unsigned char)(LCD1602_IO_D4) << 4)); \
		lcd1602_falling_edge(); \
		/* 读低 4 位 */ \
		LCD1602_IO_D7 = 1; \
		LCD1602_IO_D6 = 1; \
		LCD1602_IO_D5 = 1; \
		LCD1602_IO_D4 = 1; \
		lcd1602_wait_nop(); \
		lcd1602_rising_edge(); \
		lcd1602_wait_nop(); \
		(dat) |= (((unsigned char)(LCD1602_IO_D7) << 3) | ((unsigned char)(LCD1602_IO_D6) << 2) | ((unsigned char)(LCD1602_IO_D5) << 1) | (unsigned char)(LCD1602_IO_D4)); \
		lcd1602_falling_edge(); \
	} while(0)

#endif

#if (0==1)
// 什么都不做，仅是为了让我的编辑器能够正常处理之后的缩进
void ();
#endif


/*----------  LCD1602 指令集  ----------*/

// 1. 清屏 (Clear Display)
// 清空液晶显示屏的显示内容，即置 DDRAM 所有字节为 20H (空格字符 ' ')
// 光标将撤回显示屏左上方，地址计数器 (AC) 将设为 0，重置 I/D = 0
#define LCD1602_CMD_CLD() 0x01
#define LCD1602_XCMD_CLD LCD1602_CMD_CLD

// 2. 光标归位 (Return Home)
// 光标将撤回显示屏左上方，地址计数器 (AC) 将设为 0，DDRAM 内容保持不变
#define LCD1602_CMD_RET() 0x02
#define LCD1602_XCMD_RET LCD1602_CMD_RET

// 3. 输入模式设置 (Entry Mode Set)
// 设定每次处理 1 个字节后 (DDRAM 或是 CGRAM) 光标、屏幕以及指针的移动方向
// I/D: 光标移动方向 向右 / 向左 (H/L)
//      DDRAM 地址   自增 / 自减 (H/L)
//      I/D 在读写操作中均生效
// SH : 屏幕是否移动 移动 / 固定 (H/L)
//      SH 仅当写 DDRAM 时生效，读 DDRAM 或是对 CGRAM 操作时屏幕均不会移动
#define LCD1602_CMD_ENT(ID,SH) (0x04 | ((ID) ? 2 : 0) | !!(SH))
#define LCD1602_XCMD_ENT(mask) (0x04 | (mask))
#define LCD1602_ENTRY_ADDR_INCREASE 2
#define LCD1602_ENTRY_ADDR_DECREASE 0
#define LCD1602_ENTRY_SCREEN_MOVE   1
#define LCD1602_ENTRY_SCREEN_HOLD   0
// LCD1602_CMD_ENT(1, 0)
// 等价于 LCD1602_XCMD_ENT(LCD1602_ENTRY_ADDR_INCREASE)
// 或     LCD1602_XCMD_ENT(LCD1602_ENTRY_ADDR_INCREASE | LCD1602_ENTRY_SCREEN_HOLD)

// 4. 显示开关控制 (Display ON/OFF Control)
// 控制整个屏幕的显示是否打开，是否显示光标，光标是否闪烁
// D: 显示     开 / 关 (H/L)
// C: 光标     开 / 关 (H/L)
// B: 光标闪烁 开 / 关 (H/L)
// NOTE: C 控制是否在光标位置显示下划线，B 独立控制光标位置 (整个字符) 是否闪烁，二者互不干扰
#define LCD1602_CMD_DSP(D,C,B) (0x08 | ((D) ? 4 : 0) | ((C) ? 2 : 0) | !!(B))
#define LCD1602_XCMD_DSP(mask) (0x08 | (mask))
#define LCD1602_DISPLAY_ON   4
#define LCD1602_CURSOR_ON    2
#define LCD1602_CURSOR_SHOW  2
#define LCD1602_CURSOR_BLINK 1
// LCD1602_CMD_DSP(1, 0, 1)
// 等价于 LCD1602_XCMD_DSP(LCD1602_DISPLAY_ON | LCD1602_CURSOR_BLINK)

// 5. 移动光标或屏幕 (Cursor or Display Shift)
// 移动整个屏幕，或是移动光标，而不对 DDRAM 内的数据进行修改
// S/C: 移动屏幕 / 移动光标 (H/L)
// R/L: 右移 / 左移 (H/L)
#define LCD1602_CMD_SFT(SC,RL) (0x10 | ((SC) ? 8 : 0) | ((RL) ? 4 : 0))
#define LCD1602_XCMD_SFT(mask) (0x10 | (mask))
#define LCD1602_SHIFT_SCREEN 8
#define LCD1602_SHIFT_CURSOR 0
#define LCD1602_SHIFT_RIGHT  4
#define LCD1602_SHIFT_LEFT   0
// LCD1602_CMD_SFT(1, 0)
// 等价于 LCD1602_XCMD_SFT(LCD1602_SHIFT_SCREEN | LCD1602_SHIFT_LEFT)

// 6. 功能设定 (Function Set)
// 设置芯片的数据总线，屏幕上显示的行数，以及点阵字体高度
// DL: 数据总线 8 位 / 4 位 (H/L)
// N : 显示行数 2 行 / 1 行 (H/L)
// F : 点阵字体 5*11 /  5*8 (H/L)
// #define LCD1602_CMD_FNS(DL,N,F) (0x10 | ((DL) << 4) | ((N) << 3) | ((F) << 2))
#ifdef LCD1602_DATALINE_8_BITS
#define LCD1602_CMD_FNS(N,F) (0x30 | ((N) ? 8 : 0) | ((F) ? 4 : 0))
#define LCD1602_XCMD_FNS(mask) (0x30 | (mask))
#else
#define LCD1602_CMD_FNS(N,F) (0x20 | ((N) ? 8 : 0) | ((F) ? 4 : 0))
#define LCD1602_XCMD_FNS(mask) (0x20 | (mask))
#endif
#define LCD1602_TWO_LINES   8
#define LCD1602_SINGLE_LINE 0
#define LCD1602_LARGE_FONT  4
#define LCD1602_SMALL_FONT  0
#define LCD1602_NORMAL_FONT 0
// LCD1602_CMD_FNS(1, 1)
// 等价于 LCD1602_XCMD_FNS(LCD1602_TWO_LINES | LCD1602_LARGE_FONT)

// 7. 设置 CGRAM 地址 (Set CGRAM Address)
// 设置目标 CGRAM 地址
// AC: 目标 CGRAM 地址
#define LCD1602_CMD_CGR(AC) (0x40 | ((AC) & 0x3f))
#define LCD1602_XCMD_CGR LCD1602_CMD_CGR

// 8. 设置 DDRAM 地址 (Set DDRAM Address)
// 设置地址计数器 AC 的值
// AC: 目标 DDRAM 地址
// 当显示行数为 1 行 (N=0) 时 DDRAM 的地址范围是 00H~4FH
// 当显示行数为 2 行 (N=1) 时 DDRAM 的地址范围是 00H~27H, 40H~67H
#define LCD1602_CMD_DDR(AC) (0x80 | (AC))
#define LCD1602_XCMD_DDR LCD1602_CMD_DDR

// 9. 是否繁忙 / AC 地址值
// #define LCD1602_BUSY_FLAG 0x80
#define LCD1602_AC_ADDRESS_MASK 0x7f
#define lcd1602_ac_address() (lcd1602_read_state() & LCD1602_AC_ADDRESS_MASK)

// 10. 向 DDRAM/CGRAM 写数据 (通过函数实现)
// 11. 从 DDRAM/CGRAM 读数据 (通过函数实现)


/*----------  内部变量导出  ----------*/

// 初始化 LCD 的默认配置信息
// 当调用 lcd1602_init 方法时，用户自定义的配置内容会覆盖对应的成员
// 外部初始化 LCD 时，可自己定义对应的结构体，也可通过修改该结构体对应成员来设置初始参数
// XXX: 在未来，这个可用于全局配置，实现在现有配置上累加新配置
// XXX: 如切换光标下划线的显示状态，但不对光标的闪烁状态进行更改
extern LCD1602_INIT_DATA lcd1602_config;


/*----------  函数原型  ----------*/

// 写命令
void lcd1602_write_cmd(LCD1602_CMD cmd);
// 写数据 (到 DDRAM 或 CGRAM)
void lcd1602_write_data(LCD1602_DATA dat);
// 读状态 (BF 以及 AC 地址值)
LCD1602_STATE lcd1602_read_state();
// 读数据 (从 DDRAM 或 CGRAM)
LCD1602_DATA lcd1602_read_data();
// 判断 LCD1602 繁忙状态
LCD1602_STATE lcd1602_busy();
// 同步等待直到 LCD1602 不再繁忙
void lcd1602_waitBusy();
// 执行初始化
void lcd1602_init(LCD1602_INIT_DATA * p_init_data);
// 清屏
void lcd1602_cld();
// 光标回到初始位置
void lcd1602_ret();
// 设置光标位置
void lcd1602_setcursor(LCD1602_DATA x, LCD1602_DATA y);
// 光标左移
void lcd1602_cursor_left();
// 光标右移
void lcd1602_cursor_right();
// 屏幕左移
void lcd1602_screen_left();
// 屏幕右移
void lcd1602_screen_right();
// 打印字符串 (使用 NUL 截断)
void lcd1602_print(char * s);
// 打印字符串 (使用固定长度，不会被 NUL 截断)
void lcd1602_nprint(char * s, int n);
// 在字库中构造生成对应的字符
void lcd1602_define(LCD1602_DATA char_code, LCD1602_DATA bytes[8]);

#endif	/* __LCD1602__ */
